﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.Caching;
using System.ComponentModel.Composition.Caching.AttributedModel;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using Microsoft.Internal;

namespace System.ComponentModel.Composition
{
    /// <summary>
    ///     A mutable collection of ComposablePartCatalogs.  The events are called back using the
    ///     synchronization context created when the instance is constructed.
    /// </summary>
    /// <remarks>
    ///     This type is thread safe.
    /// </remarks>
    public class AggregatingComposablePartCatalog : ComposablePartCatalog, INotifyComposablePartCatalogChanged, ICachedComposablePartCatalog
    {
        private Lock _lock = new Lock();
        private ComposablePartCatalogCollection _catalogs = null;
        private readonly SynchronizationContext _syncContext = null;
        private volatile bool _isDisposed = false;
        private EventHandler<ComposablePartCatalogChangedEventArgs> _changedEventStorage;
        private EventHandler<EventArgs> _changingEventStorage;
        private IQueryable<ComposablePartDefinition> _partsQuery;
        private bool _readFromCache;

        /// <summary>
        ///     Initializes a new instance of the <see cref="AggregatingComposablePartCatalog"/> class.
        /// </summary>
        public AggregatingComposablePartCatalog()
            : this((IEnumerable<ComposablePartCatalog>)null)
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="AggregatingComposablePartCatalog"/> class
        ///     with the specified catalogs.
        /// </summary>
        /// <param name="catalogs">
        ///     An <see cref="IEnumerable{T}"/> of <see cref="ComposablePartCatalog"/> objects to add
        ///     to the <see cref="AggregatingComposablePartCatalog"/>; or <see langword="null"/> to 
        ///     create an <see cref="AggregatingComposablePartCatalog"/> that is empty.
        /// </param>
        /// <exception cref="ArgumentException">
        ///     <paramref name="catalogs"/> contains an element that is <see langword="null"/>.
        /// </exception>
        public AggregatingComposablePartCatalog(IEnumerable<ComposablePartCatalog> catalogs)
        {
            Requires.NullOrNotNullElements(catalogs, "catalogs");

            this._syncContext = SynchronizationContext.Current;
            this._catalogs = new ComposablePartCatalogCollection(catalogs, this.OnChanging, this.OnChanged);
            this._partsQuery = this._catalogs.AsQueryable().SelectMany(catalog => catalog.Parts);
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="AggregatingComposablePartCatalog"/> class 
        ///     with the specified cache.
        /// </summary>
        /// <param name="cache">
        ///     The <see cref="ComposablePartCatalogCache"/> to read the <see cref="AggregatingComposablePartCatalog"/> 
        ///     from.
        /// </param>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="cache"/> is <see langword="null"/>.
        /// </exception>
        public AggregatingComposablePartCatalog(ComposablePartCatalogCache cache)
        {
            Requires.NotNull(cache, "cache");
            this._syncContext = SynchronizationContext.Current;

            IEnumerable<object> subordinateCacheTokens = cache.Metadata.ReadEnumerable<object>(AttributedCacheServices.CacheKeys.SubordinateTokens);
            List<ComposablePartCatalog> subordinateCatalogs = new List<ComposablePartCatalog>();
            foreach (object subordinateCacheToken in subordinateCacheTokens)
            {
                subordinateCatalogs.Add(cache.Reader.ReadCatalog(subordinateCacheToken));
            }

            this._catalogs = new ComposablePartCatalogCollection(subordinateCatalogs, OnChanging, OnChanged);
            this._partsQuery = this._catalogs.AsQueryable().SelectMany(catalog => catalog.Parts);
            this._readFromCache = true;
        }

        /// <summary>
        ///    Caches the catalog using the specified cache writer
        /// </summary>
        /// <param name="writer">
        ///    The <see cref="ComposablePartCatalogCacheWriter"/> to use to cache the 
        ///    <see cref="AggregatingComposablePartCatalog"/>.
        /// </param>
        /// <returns></returns>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregatingComposablePartCatalog"/> has been disposed of.
        /// </exception>
        public object CacheCatalog(ComposablePartCatalogCacheWriter writer)
        {
            ThrowIfDisposed();

            Requires.NotNull(writer, "writer");

            List<object> subordinateCacheTokens = new List<object>();
            foreach (ComposablePartCatalog catalog in this.Catalogs)
            {
                ICachedComposablePartCatalog cachedCatalog = catalog as ICachedComposablePartCatalog;
                if (cachedCatalog == null)
                {
                    throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Strings.CatalogIsNotCacheable, catalog.GetType()));
                }
                object subordinateCacheToken = cachedCatalog.CacheCatalog(writer);
                subordinateCacheTokens.Add(subordinateCacheToken);
            }

            IDictionary<string, object> metadata = new Dictionary<string, object>();
            metadata.WriteEnumerable(AttributedCacheServices.CacheKeys.SubordinateTokens, subordinateCacheTokens);

            object cacheToken = writer.WriteCache(
                this.GetType(),
                null,
                metadata,
                null);

            writer.WriteRootCacheToken(cacheToken);
            return cacheToken;
        }

        /// <summary>
        ///     Gets a value indicating whether the underlying cache is up-to-date.
        /// </summary>
        /// <value>
        ///     <see langword="true"/> if the underlying <see cref="ComposablePartCatalogCache"/> 
        ///     is up-to-date; otherwise, <see langword="false"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregatingComposablePartCatalog"/> has been disposed of.
        /// </exception>
        public bool IsCacheUpToDate
        {
            get
            {
                ThrowIfDisposed();

                if (!this._readFromCache)
                {
                    return false;
                }

                if (this._catalogs.HasChanged)
                {
                    return false;
                }

                foreach (ComposablePartCatalog catalog in this.Catalogs)
                {
                    ICachedComposablePartCatalog cachedCatalog = catalog as ICachedComposablePartCatalog;
                    if ((cachedCatalog == null) || (!cachedCatalog.IsCacheUpToDate))
                    {
                        return false;
                    }
                }
                return true;
            }
        }


        /// <summary>
        ///     Gets the part definitions of the catalog.
        /// </summary>
        /// <value>
        ///     A <see cref="IQueryable{T}"/> of <see cref="ComposablePartDefinition"/> objects of the 
        ///     <see cref="AggregatingComposablePartCatalog"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregatingComposablePartCatalog"/> has been disposed of.
        /// </exception>
        public override IQueryable<ComposablePartDefinition> Parts
        {
            get
            {
                this.ThrowIfDisposed();
                return this._partsQuery;
            }
        }

        /// <summary>
        ///     Returns the export definitions that match the conditions defined by the specified constraint.
        /// </summary>
        /// <param name="constraint">
        ///     A <see cref="Expression{TDelegate}"/> containing a <see cref="Func{T, TResult}"/> 
        ///     that defines the conditions of the <see cref="ExportDefinition"/> objects to return.
        /// </param>
        /// <returns>
        ///     An <see cref="IEnumerable{T}"/> of <see cref="Tuple{TFirst, TSecond}"/> containing the 
        ///     <see cref="ExportDefinition"/> objects and their associated 
        ///     <see cref="ComposablePartDefinition"/> objects that matche the conditions defined 
        ///     by <paramref name="constraint"/>.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        ///     <paramref name="constraint"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregatingComposablePartCatalog"/> has been disposed of.
        /// </exception>
        public override IEnumerable<Tuple<ComposablePartDefinition, ExportDefinition>> GetExports(Expression<Func<ExportDefinition, bool>> constraint)
        {
            ThrowIfDisposed();

            Requires.NotNull(constraint, "constraint");

            // delegate the query to each catalog and merge the results.
            return this._catalogs.SelectMany(catalog => catalog.GetExports(constraint));
        }

        /// <summary>
        ///     Gets the underlying catalogs of the catalog.
        /// </summary>
        /// <value>
        ///     An <see cref="ICollection{T}"/> of underlying <see cref="ComposablePartCatalog"/> objects
        ///     of the <see cref="AggregatingComposablePartCatalog"/>.
        /// </value>
        /// <exception cref="ObjectDisposedException">
        ///     The <see cref="AggregatingComposablePartCatalog"/> has been disposed of.
        /// </exception>
        public ICollection<ComposablePartCatalog> Catalogs
        {
            get
            {
                this.ThrowIfDisposed();
                return this._catalogs;
            }
        }

        protected override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    if (!this._isDisposed)
                    {
                        bool disposeLock = false;
                        ComposablePartCatalogCollection catalogs = null;
                        try
                        {
                            using (new WriteLock(this._lock))
                            {
                                if (!this._isDisposed)
                                {
                                    catalogs = this._catalogs;
                                    this._catalogs = null;

                                    disposeLock = true;
                                    this._isDisposed = true;
                                }
                            }
                        }
                        finally
                        {
                            if (disposeLock)
                            {
                                this._lock.Dispose();
                            }

                            if (catalogs != null)
                            {
                                catalogs.Dispose();
                            }
                        }
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        private void OnChanged(object sender, ComposablePartCatalogChangedEventArgs e)
        {
            EventHandler<ComposablePartCatalogChangedEventArgs> changedEvent;

            using (new ReadLock(this._lock))
            {
                changedEvent = this._changedEventStorage;
            }

            if (changedEvent != null)
            {
                SynchronizationContextHelpers.ExecuteOnCreationThread(this._syncContext, delegate
                {
                    changedEvent(sender, e);
                });
            }
        }

        private void OnChanging(object sender, EventArgs e)
        {
            EventHandler<EventArgs> changingEvent;

            using (new ReadLock(this._lock))
            {
                changingEvent = this._changingEventStorage;
            }

            if (changingEvent != null)
            {
                SynchronizationContextHelpers.ExecuteOnCreationThread(this._syncContext, delegate
                {
                    changingEvent(sender, e);
                });
            }
        }

        private void ThrowIfDisposed()
        {
            if (this._isDisposed)
            {
                throw new ObjectDisposedException(this.GetType().ToString());
            }
        }

        #region INotifyComposablePartCatalogChanged Members

        /// <summary>
        /// Notify when the contents of the Catalog has changed.
        /// </summary>
        public event EventHandler<ComposablePartCatalogChangedEventArgs> Changed
        {
            add
            {
                this.ThrowIfDisposed();

                using (new WriteLock(this._lock))
                {
                    this._changedEventStorage += value;
                }
            }
            remove
            {
                ThrowIfDisposed();

                using (new WriteLock(this._lock))
                {
                    this._changedEventStorage -= value;
                }
            }
        }

        /// <summary>
        /// Notify when the contents of the Catalog is about to change.
        /// </summary>
        public event EventHandler<EventArgs> Changing
        {
            add
            {
                this.ThrowIfDisposed();

                using (new WriteLock(this._lock))
                {
                    this._changingEventStorage += value;
                }
            }
            remove
            {
                this.ThrowIfDisposed();

                using (new WriteLock(this._lock))
                {
                    this._changingEventStorage -= value;
                }
            }
        }
        #endregion
    }
}
